Java方法完整调用链生成工具 您所在的位置:网站首页 idea 查看调用链快捷键 Java方法完整调用链生成工具

Java方法完整调用链生成工具

2023-07-01 08:30| 来源: 网络整理| 查看: 265

1. 前言

在很多场景下,如果能够生成Java代码中方法之间的调用链,是很有帮助的,例如分析代码执行流程、确认被修改代码的影响范围、代码审计/漏洞分析等。

IDEA提供了显示调用指定Java方法向上的完整调用链的功能,可以通过“Navigate -> Call Hierarchy”菜单(快捷键:Ctrl+Alt+H)使用;Eclipse也提供了相同的功能。但以上都需要针对每个方法进行手工处理,不支持对方法进行过滤或者其他扩展功能。

以下实现了一个工具,能够批量生成指定Java方法向下的完整调用链,对于关注的Java方法,能够生成其向下调用的方法信息,及被调用方法再向下调用的方法,直到最下层被调用的方法。

也可以生成调用指定Java类方法向上的完整调用链,对于关注的Java类的方法,能够生成调用对应方法的方法信息,及调用上述方法的信息,直到最上层未被其他方法调用的方法(通常是对外提供的服务,或定时任务等)。

该工具生成的Java方法完整调用链中,支持显示相关的包名、类名、方法名、方法参数、调用者源代码行号、方法注解、循环调用,入口方法。

该工具支持生成某个方法到起始方法之间的调用链,也支持根据关键字查找关注的方法,生成其到起即方法之间的调用链。

最新说明可查看https://github.com/Adrninistrator/java-all-call-graph。

当前项目提供了插件功能,可用于为Java代码自动生成UML时序图,可参考https://github.com/Adrninistrator/gen-java-code-uml-sequence-diagram。

2. 输出结果示例 2.1. 调用指定类方法向上的完整调用链示例

调用指定类方法向上的完整调用链如下所示:

[0]#DestClass.destfunc() [1]# ClassA3.funcA3() (ClassA3:10) [2]# ClassA2.funcA2() (ClassA2:19) [3]# ClassA1.funcA1() (ClassA1:23) !entry! [1]# ClassB1.funcB1() (ClassB1:57) !entry! [1]# ClassC2.funcC2() (ClassC2:31) [2]# ClassC1.funcC1() (ClassC1:9) !entry!

以上对应的调用关系如下所示:

在这里插入图片描述

调用指定类方法向上的完整调用链输出结果格式类似一棵树,每行代表一个调用者Java方法,与实际的代码执行顺序无关,前面的数字越大代表调用层级越靠上,0代表被调用的指定类中的方法。

每行后部的“(TestClass:39)”格式的类名及数字代表当前调用者类名,及调用者方法对应的源代码行号。

对于不被其他方法调用的方法,认为是入口方法,在对应行的最后会显示“!entry!”。

以下为使用该工具生成的调用Mybatis的MyBatisExceptionTranslator类的部分方法向上完整调用链(方法参数太长,已省略):

在这里插入图片描述

IDEA使用技巧:在IntelliJ IDEA中,打开“Navigate Class...”窗口,即根据类名进入对应代码文件的窗口后,若输入[类名]:[行号]格式的内容并回车,可打开对应的代码文件并跳转到对应的行号。

2.2. 指定方法向下完整调用链示例

指定方法向下完整调用链如下所示:

[0]#SrcClass.srcfunc() [1]# [SrcClass:15] ClassA1.funcA1() [2]# [ClassA1:27] ClassA2a.funcA2a() [2]# [ClassA1:59] ClassA2b.funcA2b() [3]# [ClassA2b:39] ClassA3.funcA3() [1]# [SrcClass:17] ClassB1.funcB1() [1]# [SrcClass:23] ClassC1.funcC1() [2]# [ClassC1:75] ClassC2.funcC2()

以上对应的调用关系如下所示:

在这里插入图片描述

指定方法向下完整调用链输出结果类似一棵树,每行代表一个被调用者Java方法,与实际的代码执行顺序一致,前面的数字越大代表调用层级越靠下,0代表指定方法。

每行前部的“[TestClass:39]”格式的类名及数字,代表当前调用者类名,及调用者方法及对应的源代码行号。

以下为使用该工具生成的Mybatis的BatchExecutor:doUpdate()方法向下的完整调用链:

在这里插入图片描述

除此之外,当方法指定了注解时,也可以显示在结果中,格式为“@xxx”;

当出现方法循环调用时,会显示出现循环调用的方法,格式为“!cycle[x]!”。

3. 使用说明 3.1. 依赖环境

该工具将Java方法调用关系写入文件之后,会将数据保存在数据库中,需要访问MySQL数据库(理论上支持其他数据库,但可能需要对SQL语句进行调整)。

所使用的数据库用户需要有DML读写权限,及DDL权限(需要执行CREATE TABLE、TRUNCATE TABLE操作)。

3.2. 引入组件

在使用该工具前,首先需要在对应的项目引入该工具组件的依赖,将其引入到test模块或使用provided类型,可以避免发布到服务器中。

Gradle testImplementation 'com.github.adrninistrator:java-all-call-graph:0.5.1' Maven com.github.adrninistrator java-all-call-graph 0.5.1 provided

最新版本号可查看https://search.maven.org/artifact/com.github.adrninistrator/java-all-call-graph。

对应代码地址为https://github.com/Adrninistrator/java-all-call-graph。

建议在需要生成方法调用链的项目中分别引入依赖,可以使每个项目使用单独的配置,不会相互影响。

该工具仅引入了log4j-over-slf4j组件,在引入该工具组件的项目中,还需要引入log4j2、logback等日志组件,且保证配置正确,能够在本地正常运行。

3.3. 执行步骤

以下所述执行步骤,需要在IDE中执行。

3.3.1. 总体步骤

该工具的总体使用步骤如下:

a. 将后续步骤使用的几个启动类对应的Java文件,及配置文件解压到当前Java项目的test模块的对应目录中,该步骤只需要执行一次(当组件更新时需要再次执行,以释放新的文件);b. 调用增强后的java-callgraph2.jar(详细内容见后续“原理说明”部分),解析指定jar包中的class文件,将Java方法调用关系写入文件;从该文件读取Java方法调用关系,再写入MySQL数据库;c.1 需要生成调用指定类的向上完整方法调用链时,从数据库读取方法调用关系,再将完整的方法调用链写入文件;c.2 需要生成指定方法的向下完整方法调用链时,从数据库读取方法调用关系,再将完整的方法调用链写入文件;

如下图所示:

在这里插入图片描述

3.3.2. 释放启动类及配置文件

当前步骤在每个Java项目只需要执行一次。

当组件升级后,若对配置文件有新增或修改,则需要再执行当前步骤,否则可能会因为缺少配置文件导致执行失败。

执行当前步骤时,需要执行main()方法的类名如下:

com.adrninistrator.jacg.unzip.UnzipFile

需要选择classpath对应模块为test。

执行以上类后,会将java-all-callgraph.jar中保存配置文件的jacg_config、jacg_extensions、jacg_find_keyword、jacg_sql目录,保存启动类(下文涉及的Test…类)的“test/jacg”目录,分别释放到当前Java项目的test模块的resources、java目录中(仅在本地生效,避免发布到服务器中)。

若当前Java项目存在“src/test”或“src/unit.test”目录,则将配置文件与Java文件分别释放在该目录的resources、java目录中;

若当前Java项目不存在以上目录,则将上述文件释放在“~jacg-[当前时间戳]”目录中,之后需要手工将对应目录拷贝至test模块对应目录中。

当目标文件不存在时,则会进行释放;若目标文件已存在,则不会覆盖。

3.3.3. 生成Java方法调用关系并写入数据库

在生成Java方法调用关系并写入数据库之前,需要确保需要分析的jar包或war包已存在,对于使用源码通过构建工具生成的jar/war包,或者Maven仓库中的jar包,均可支持(需要是包含.class文件的jar包)。

当需要解析的jar/war包中的class文件内容发生变化时,需要重新执行当前步骤,以重新获取对应jar/war包中的Java方法调用关系,写入文件及数据库;若需要解析的jar/war包文件未发生变化,则不需要重新执行当前步骤。

在后续生成Java方法完整调用链时,若发现指定的jar包未入库,或内容发生了改变,则工具会提示需要先执行当前步骤生成方法调用关系并入库。

执行当前步骤时,需要执行main()方法的类名如下:

test.jacg.TestRunnerWriteDb

需要选择classpath对应模块为test。

当前步骤执行的操作及使用的相关参数如下图所示:

在这里插入图片描述

b.1 调用增强后的java-callgraph2.jar中的类的方法

TestRunnerWriteDb类读取配置文件config.properties中的参数:

call.graph.jar.list:等待解析的jar包路径列表,各jar包路径之间使用空格分隔(若路径中包含空格,则需要使用""包含对应的路径)

将第1个jar包路径后面加上“.txt”作为本次保存Java方法调用关系文件路径;

设置JVM参数“output.file”值为本次保存Java方法调用关系文件的路径,调用增强后的java-callgraph2.jar中的类的方法,通过方法的参数传递上述jar包路径列表;

b.2 解析指定jar包

增强后的java-callgraph2.jar中的类的方法开始解析指定的jar包;

b.3 将Java方法调用关系写入文件

增强后的java-callgraph2.jar中的类的方法将解析出的Java方法调用关系写入指定的文件中;

b.4 读取Java方法调用关系文件

TestRunnerWriteDb类读取保存Java方法调用关系的文件,文件路径即第1个jar包路径加“.txt”;

b.5 将Java方法调用关系写入数据库

TestRunnerWriteDb类读取配置文件i_allowed_class_prefix.properties,该文件中指定了需要处理的类名前缀,可指定包名,或包名+类名,示例如下:

com.test com.test.Test1

读取配置文件config.properties中的参数:

app.name:当前应用名称,对应数据库表名后缀,该参数值中的分隔符不能使用“-”,需要使用“_”

thread.num:写入数据库时并发处理的线程数量,也是数据源连接池数量

db.driver.name:数据库驱动类名

db.url:数据库URL,使用MySQL时,url需要指定rewriteBatchedStatements=true,开启批量插入,提高效率

db.username:数据库用户名

db.password:数据库密码

input.ignore.other.package:忽略其他包的开关,值为true/false;当开关为开时,仅将i_allowed_class_prefix.properties中指定的类名前缀相符的类调用关系写入数据库;当开关为关时,所有的类调用关系都写入数据库

向数据库写入数据库前,会判断对应数据库表是否存在,若不存在则创建,之后会执行“TRUNCATE TABLE”操作清空表中的数据;

根据配置文件config.properties中的input.ignore.other.package参数值及配置文件i_allowed_class_prefix.properties,将Java方法调用关系逐条写入数据库中;

增强后的java-callgraph2.jar除了会将Java方法调用关系写入文件外,还会将各个方法上的注解信息写入文件(文件名为保存方法调用关系的文件名加上“-annotation.txt”);TestRunnerWriteDb类也会读取对应文件,将各方法上的注解信息写入数据库中。

3.3.4. 生成调用指定类方法向上的完整调用链

执行当前步骤之前,需要确认Java方法调用关系已成功写入数据库中。

执行当前步骤时,需要执行main()方法的类名如下:

test.jacg.TestRunnerGenAllGraph4Callee

需要选择classpath对应模块为test。

当前步骤执行的操作及使用的相关参数如下图所示:

在这里插入图片描述

c.1.1 从数据库读取Java方法调用关系

TestRunnerGenAllGraph4Callee类读取配置文件o_g4callee_class_name.properties,该文件中指定了需要生成向上完整调用链的类名;

类名可指定简单类名或完整类名;若存在同名类,则需要指定完整类名;

示例如下:

Test1 com.test.Test1

读取配置文件config.properties中的参数:

thread.num:从数据库并发读取数据的线程数量,也是数据源连接池数量;若o_g4callee_class_name.properties配置文件中的记录数比该值小,则会使用记录数覆盖该参数值

以下参数说明略:app.name、db.driver.name、db.url、db.username、db.password

c.1.2 将方法完整调用链(向上)写入文件

对于配置文件o_g4callee_class_name.properties中指定的类,对每个类生成一个对应的文件,文件名为“[类名].txt”,在某个类对应的文件中,会为对应类的每个方法生成向上完整调用链;

以上文件名示例为“TestClass1.txt”;

每次执行时会生成一个新的目录,用于保存输出文件,目录名格式为“~jacg_output_for_callee/[yyyyMMdd-HHmmss.SSS]”;

读取配置文件config.properties中的参数:

call.graph.output.detail:输出文件中调用关系的详细程度,1: 最详细,包含完整类名+方法名+方法参数,2: 中等,包含完整类名+方法名,3: 最简单,包含简单类名(对于同名类展示完整类名)+方法名,示例如下

call.graph.output.detail参数值显示示例1com.test.Test1.func1(java.lang.String)2com.test.Test1.func13Test1.func1

show.method.annotation:调用链中是否显示方法上的注解开关,值为true/false;当开关为开时,会显示当前方法上的全部注解的完整类名,格式为“[方法信息]@注解1@注解2…”

gen.combined.output:是否生成调用链的合并文件开关,值为true/false;当开关为开时,在为各个类生成了对应的调用链文件后,会生成一个将全部文件合并的文件,文件名为“~all-4callee.txt”

show.caller.line.num:生成调用链时,是否需要显示调用者源代码行号开关,值为true/false;当开关为开时,会在向上的调用链每行后部显示当前调用者类名,及调用者方法对应的源代码行号,如“(TestClass:39)”

gen.upwards.methods.file:生成向上的调用链时,是否需要为每个方法生成单独的文件开关,值为true/false;当开关为开时,会为o_g4callee_class_name.properties中指定的每个类的每个方法单独生成一个文件,保存在“~jacg_output_for_callee/[yyyyMMdd-HHmmss.SSS]/methods”目录中,文件名格式为“[类名]@[方法名]@[完整方法名HASH+长度].txt”

3.3.5. 生成指定方法向下完整调用链

执行当前步骤之前,需要确认Java方法调用关系已成功写入数据库中。

3.3.5.1. 生成所有的调用链

执行当前步骤时,需要执行main()方法的类名如下:

test.jacg.TestRunnerGenAllGraph4Caller

需要选择classpath对应模块为test。

当前步骤执行的操作及使用的相关参数如下图所示:

在这里插入图片描述

c.2.1 从数据库读取Java方法调用关系

TestRunnerGenAllGraph4Caller类读取配置文件o_g4caller_entry_method.properties,该文件中指定了需要生成向下完整调用链的类名与方法名前缀,格式为[类名]:[方法名] [起始代码行号]-[结束代码行号],或[类名]:[方法名+参数] [起始代码行号]-[结束代码行号];

[起始代码行号]-[结束代码行号]为可选参数,若不指定则输出指定的整个方法向下的方法完整调用链,若指定则输出方法指定行号范围内向下的方法完整调用链,即 >= 起始代码行号 且 @Override public void run() { f2(); } }).start(); }

原始java-callgraph生成的方法调用关系中,f1()调用f2(),及f2()向下调用的关系会缺失;

对于使用命名类形式的Runnable实现类在线程中执行操作的情况,存在相同的问题,原方法调用线程中执行的方法,及继续向下的调用关系会缺失。

Callable实现类线程调用

与Runnable实现类线程调用情况类似,略。

Thread子类线程调用

与Runnable实现类线程调用情况类似,略。

lambda表达式(含线程调用等)

假如f1()方法中使用lambda表达式的形式在线程中执行操作,在线程中执行了f2()方法,如下所示:

private void f1() { new Thread(() -> f2()).start(); }

原始java-callgraph生成的方法调用关系中,f1()调用f2(),及f2()向下调用的关系会缺失;

对于其他使用lambda表达式的情况,存在相同的问题,原方法调用lambda表达式中执行的方法,及继续向下的调用关系会缺失。

Stream调用

在使用Stream时,通过xxx::func方式调用方法,原始java-callgraph生成的方法调用关系中会缺失。如以下示例中,当前方法调用当前类的map2()、filter2(),及TestDto1类的getStr()方法的调用关系会缺失。

list.stream().map(this::map2).filter(this::filter2).collect(Collectors.toList()); list.stream().map(TestDto1::getStr).collect(Collectors.toList()); 父类调用子类的实现方法

假如存在抽象父类Abstract1,及其非抽象子类ChildImpl1,若在某个类Class1中引入了抽象父类Abstract1,实际为子类ChildImpl1的实例(使用Spring时的常见场景),在其方法Class1.func1()中调用了Abstract1.fa()方法;

原始java-callgraph生成的方法调用关系中,只包含Class1.func1()调用Abstract1.fa()的关系,Class1.func1()调用ChildImpl1.fa()的关系会缺失。

子类调用父类的实现方法

假如存在抽象父类Abstract1,及其非抽象子类ChildImpl1,若在ChildImpl1.fc1()方法中调用了父类Abstract1实现的方法fi();

原始java-callgraph生成的方法调用关系中,ChildImpl1.fc1()调用Abstract1.fi()的关系会缺失。

6.2. Java方法完整调用链生成 数据库表

在获取了Java方法调用关系之后,将其保存在数据库中,可查看java-all-callgraph.jar释放的~jacg_sql目录中的.sql文件,相关数据库表如下所示:

表名前缀注释作用class_name_类名信息表保存相关类的完整类名及简单类名method_annotation_方法注解表保存方法及方法上的注解信息method_call_方法调用关系表保存各方法之间调用信息jar_info_jar包信息表保存用于解析方法调用关系的jar包信息extended_data_自定义数据表manual_add_extended_data_手工添加的自定义数据表

上述数据库表在创建时使用表名前缀加上配置文件config.properties中的app.name参数值。

该工具会主要从方法调用关系表中逐级查询数据,生成完整的方法调用链。

禁用sql_mode中的ONLY_FULL_GROUP_BY

在MySQL 5.7中,执行类似以下SQL语句时,使用默认配置会出现以下报错:

select distinct(callee_method_hash),callee_full_method from method_call_xxx where callee_class_name= 'xxx' order by callee_method_name Expression #1 of ORDER BY clause is not in SELECT list, references column 'xxxx' which is not in SELECT list; this is incompatible with DISTINCT

说明可见https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html。

为了使MySQL支持以上查询语句,需要禁用sql_mode中的ONLY_FULL_GROUP_BY,该工具会在查询时自动禁用。

7. 无法正确处理的情况

以下情况,对应的方法找不到被调用关系,可能会被误识别为入口方法:

不是直接通过Java方法进行调用的情况(例如在XML文件中配置代码执行流程、通过注解配置代码执行流程、使用AOP处理等);未被调用的方法。 8. 多余的调用关系处理 8.1. 问题

当代码中引入了接口或抽象父类,且对应多个实现类或子类时,生成的方法完整调用链可能存在多余的调用关系。

当一个接口对应多个实现类时,若在某个类中引入了接口,并调用其方法,生成的完整调用链中,可能将当前类未使用的其他实现类相关的调用关系也包含进来;

当一个抽象父类对应多个非抽象子类时,若在某个类中引入了抽象父类,并调用其方法,生成的完整调用链中,可能将当前类未使用的其他非抽象子类相关的调用关系也包含进来。

当代码中使用工厂模式获取某个接口/抽象父类的实现类/非抽象子类时,也可能会出现类似的问题。

8.2. 解决

当存在以上情况时,该工具会在当前目录生成“notice_multi_ITF.md”或“notice_multi_SCC.md”文件,可按照文档中的提示,将前缀为“method_call_”的数据库表中不需要的方法调用设置为禁用。

当需要将禁用的方法调用恢复为启用时,可按照当前目录生成的“notice_disabled_ITF.md”或“notice_disabled_SCC.md”文件的说明进行操作。

9. 适用场景 9.1. 分析代码执行流程

使用该工具生成指定方法向下调用链的功能,可以将代码中复杂的方法调用转换为相对简单的方法调用链形式展示。

人工查看生成的调用链时,能够通过类名及方法名识别出对应含义。

支持将不关注的方法调用忽略,仅展示重要的方法调用。

对于分析代码执行流程有一定帮助,适合梳理交易流程、首次接触代码时熟悉流程等场景。

9.2. 确认被修改代码的影响范围

使用该工具生成指定方法向上调用链的功能,可以生成调用指定类的所有方法的调用链。

能识别入口方法,减少人工逐层确认入口方法的工作量。

可用于快速确认被修改代码的影响范围。

9.3. 应用功能拆分

在进行应用功能拆分时,需要准确定位指定功能涉及的数据库表,及使用了对应数据库表的相关入口方法。

使用该工具生成指定方法向下调用链的功能,生成指定入口方法向下的调用链,能够根据类的包名快速找到Mapper接口(使用Mybatis的场景),即可找到相关的数据库表。

使用该工具生成指定方法向上调用链的功能,生成调用指定Mapper接口向上的调用链,能够根据“!entry!”找到入口方法。

重复执行以上过程,直到没有再找到新的Mapper接口(即数据库表)和入口方法,即可确认指定功能涉及的数据库表及相关入口方法。

9.4. 代码审计/漏洞分析

在进行代码审计时,可使用该工具梳理交易流程,生成指定方法向下的调用链,查找是否有调用敏感API;或者生成指定方法向上的调用链,查找调用敏感API的场景。

在进行漏洞分析时,结合该工具生成的完整调用链辅助分析,也能提高效率。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有